home *** CD-ROM | disk | FTP | other *** search
/ Software Vault: The Gold Collection / Software Vault - The Gold Collection (American Databankers) (1993).ISO / cdr49 / mtl100je.zip / THREADS.DOC < prev    next >
Text File  |  1993-05-06  |  26KB  |  534 lines

  1.  
  2.  
  3.                                                                 Page 1
  4.  
  5.     Class DOSThread: a base class for multithreaded DOS programs.
  6.     ---------------------------------------------------------------------
  7.  
  8.       Author: John English (je@unix.brighton.ac.uk)
  9.               Department of Computing
  10.               University of Brighton
  11.               Brighton BN2 4GJ, England.
  12.  
  13.       Copyright (c) J.English 1993.
  14.  
  15.       Permission is granted to use copy and distribute the
  16.       information contained in this file provided that this
  17.       copyright notice is retained intact and that any software
  18.       or other document incorporating this file or parts thereof
  19.       makes the source code for the library of which this file
  20.       is a part freely available.
  21.  
  22.  
  23.     1. Introduction.
  24.     ----------------
  25.     Class DOSThread provides a framework for writing DOS applications
  26.     which consist of multiple (pseudo-)parallel "threads" of execution.
  27.     DOS was not designed as a multithreading operating system (in fact,
  28.     it actively hinders multithreading by being non-reentrant) but this
  29.     class allows you to create multithreaded DOS applications without
  30.     worrying about such problems (although you should read section 11
  31.     for some important caveats concerning direct calls to the BIOS).
  32.  
  33.     To create a thread using this class, you must derive a class from
  34.     it which contains the code you want to be executed in a member
  35.     function called "main".  All you have to do then is declare an
  36.     instance of your derived class and then call the member function
  37.     "run" to start it running.  Each thread will be executed in bursts
  38.     of 1 clock tick (55ms) at a time before being suspended to allow
  39.     the other threads a chance to execute.  The length of the timeslice
  40.     can be changed if necessary using the member function "timeslice".
  41.     If required, timeslicing can be disabled entirely, in which case
  42.     it is up to each thread to relinquish the use of the processor at
  43.     regular intervals so that the other threads get a chance to run.
  44.  
  45.     Threads can delay themselves for a given number of clock ticks by
  46.     using the member function "delay", they can relinquish the use of
  47.     the processor to allow other threads to execute by using the member
  48.     function "pause", and they can terminate themselves or each other
  49.     by using the member function "terminate".  It is also possible to
  50.     inspect the current state of any thread (ready-to-run, terminated,
  51.     delayed and so on) by using the member function "status" and to
  52.     wait for a thread to terminate by using the member function "wait".
  53.  
  54.     Two additional classes (DOSMonitor and DOSMonitorQueue) allow you
  55.     to derive monitor classes of your own to facilitate communication
  56.     between threads.  A monitor will normally contain data structures
  57.     which can be accessed by several threads.  You can guarantee that
  58.     only one thread at a time is executing a monitor member function
  59.     which accesses the data by calling the member function "lock" at
  60.     the start of the monitor function.  If any other thread is already
  61.     executing a monitor function guarded by a call to "lock", the
  62.     current thread will wait until it is safe to proceed.  At the end
  63.  
  64.  
  65.                                                                 Page 2
  66.  
  67.     of the monitor function, you should call "unlock" to allow any
  68.     waiting threads to proceed.  Monitors can also contain instances
  69.     of DOSMonitorQueue which allow threads to suspend themselves in a
  70.     monitor function until some condition has been fulfilled (e.g.
  71.     that a buffer isn't empty).  Some other thread executing within
  72.     the monitor can resume any suspended threads when the condition
  73.     is fulfilled (e.g. after a data item has been put into an empty
  74.     buffer).  A template class which implements a bounded buffer is
  75.     included in this distribution.  This is probably the commonest
  76.     use of monitors in most applications, so it may well not be
  77.     necessary to define any other monitor classes of your own.
  78.  
  79.     If you find this class useful or have any suggestions as to how it
  80.     can be enhanced, please contact the author at one of the addresses
  81.     given above.  E-mail and postcards will both be welcome!
  82.  
  83.  
  84.     2. Deriving a new thread "MyThread" from class DOSThread.
  85.     ---------------------------------------------------------
  86.     Every thread is created by deriving a new class from the base class
  87.     DOSThread.  Each derived thread class must provide a definition of
  88.     a member function called "main" which contains the code which the
  89.     thread will execute.  "Main" is declared like this:
  90.  
  91.         void MyThread::main ()
  92.         {
  93.             // code to be executed by your thread
  94.         }
  95.  
  96.     The constructor for your derived class "MyThread" will invoke the
  97.     constructor for DOSThread.  The constructor for DOSThread requires
  98.     a single unsigned integer parameter which specifies the size of the
  99.     stack to be allocated for the thread.  However, a default of 2048
  100.     bytes is assumed, and if this is sufficient you need not explicitly
  101.     call the DOSThread constructor at all.
  102.  
  103.     Having created a derived thread class, you can then declare instances
  104.     of this class in your program, as for example:
  105.  
  106.         MyThread thread1;           // a thread called "thread1"
  107.         MyThread threads [5];       // five identical threads
  108.  
  109.     The threads you declare will not be executed until you call the member
  110.     function "run", as follows:
  111.  
  112.         thread1.run ();
  113.  
  114.     "Run" returns a result to the calling program which is TRUE (1)
  115.     if the thread was started successfully, and FALSE (0) if it could
  116.     not be started (either because there was insufficient memory to
  117.     create the necessary data structures or because it has already
  118.     been started).  Note that you cannot call "run" from your thread
  119.     constructor since the virtual function "main" is not accessible
  120.     until you have finished executing the constructor.
  121.  
  122.     Once a thread has been started successfully, it will be executed
  123.     in parallel with the main program.  The main program effectively
  124.     becomes another thread (although it has no name, and it can only
  125.  
  126.  
  127.                                                                 Page 3
  128.  
  129.     make use of the static functions "pause" and "delay" described
  130.     below).
  131.  
  132.     The default is for each thread to be granted a "timeslice" of one
  133.     clock tick (55ms).  If a thread is still running when its timeslice
  134.     expires, it is moved to the back of the queue of ready-to-run threads
  135.     and execution of the next thread in the queue is then resumed.  The
  136.     static member function "timeslice" can be used to change the length
  137.     of the timeslices used.  "Timeslice" requires an unsigned integer
  138.     parameter specifying the desired timeslice length in clock ticks,
  139.     as for example:
  140.  
  141.         DOSThread::timeslice (18);    // timeslice once a second (18 x 55ms)
  142.  
  143.     If the parameter is zero, timeslicing is disabled.  In this case
  144.     it is up to individual threads to relinquish control to each other
  145.     by calling a member function which will cause another thread to be
  146.     scheduled.  A member function "pause" is provided for just this
  147.     purpose, and is described below.
  148.  
  149.     "Timeslice" must be called before any threads are declared; as soon
  150.     as the first thread has been declared, calls to "timeslice" will be
  151.     ignored.  This means you cannot dynamically change the length of the
  152.     timeslice during execution of the program.
  153.  
  154.  
  155.     3. Writing the member function "main".
  156.     --------------------------------------
  157.     "MyThread::main" (the main function of your derived class) will be
  158.     executed in parallel with the rest of the program once it has been
  159.     started by calling "run" as described above.  While "MyThread::main"
  160.     can be written in exactly the same way as any other function, it is
  161.     important to remember that it is sharing the processor with a number
  162.     of other threads and that if it has nothing useful to do, it should
  163.     allow some other thread to run.  The member function "pause" lets you
  164.     temporarily release the processor to another thread:
  165.  
  166.         pause ();                   // schedule another thread
  167.  
  168.     This is a static member function, so is can be called from any
  169.     point in a program as "DOSThread::pause".  Even if you are using
  170.     timeslicing, it is a good idea to call "pause" if your thread is
  171.     temporarily unable to proceed (e.g. it is waiting for a key to
  172.     be pressed), as otherwise it will do nothing useful for several
  173.     milliseconds until its timeslice expires and another thread gets
  174.     a chance to run.
  175.  
  176.     You can also make your thread wait for a fixed time by using the
  177.     static member function "delay", specifying the delay period as
  178.     a number of 55ms clock ticks:
  179.  
  180.         delay (18);                 // delay for 1 second (18 x 55ms)
  181.  
  182.     Note that "pause" and "delay" are both static member functions
  183.     which always affect the current thread.  This means that you are
  184.     not able to "pause" or "delay" any other thread.  It also means
  185.     that you can call these functions from the main program if you
  186.     need to.
  187.  
  188.  
  189.                                                                 Page 4
  190.  
  191.     When "MyThread::main" returns, the thread terminates.  You can also
  192.     terminate a thread explicitly using the member function "terminate".
  193.     If another thread (or the main program) wants to terminate "thread1",
  194.     it can do it like this:
  195.  
  196.         thread1.terminate ();
  197.  
  198.     This is potentially problematical, as you have no idea what "thread1"
  199.     is doing at the time.  A thread can also terminate itself:
  200.  
  201.         terminate ();
  202.  
  203.     which has the same effect as returning from the main function of
  204.     the thread.
  205.  
  206.  
  207.     4. Initialisation and finalisation.
  208.     -----------------------------------
  209.     When a thread is declared by the main program or by another thread
  210.     the constructor for class DOSThread is called to create the thread
  211.     and any constructor defined by your derived thread class is then
  212.     called to complete the initialisation.  Note that a thread is not
  213.     completely constructed until this sequence is complete, and in
  214.     particular this means that you cannot call "run" from inside your
  215.     derived class constructor to start the thread running immediately.
  216.  
  217.     When you reach the end of a block in which a thread was declared,
  218.     the destructor for the thread will be called.  Any destructor you
  219.     provide in your derived class is called first (while the thread
  220.     could still be running), and the standard DOSThread destructor is
  221.     then called to wait for the thread to terminate before tidying up.
  222.     This means that your destructor should not do anything which might
  223.     cause the thread to fail.  The member function "wait" allows you to
  224.     wait for the thread to terminate, and your destructor should call
  225.     this function before doing anything that might cause the thread to
  226.     fail.  In other words, your destructor should be written like this:
  227.  
  228.         MyThread::~MyThread ()
  229.         {
  230.             wait ();            // wait for thread to terminate
  231.             ...                 // do any class-specific tidying up
  232.         }
  233.  
  234.  
  235.     5. Handling "control-break" and critical errors.
  236.     ------------------------------------------------
  237.     Class DOSThread provides a simple mechanism for dealing with events
  238.     reported by DOS.  The first such event is the "control-break" key
  239.     being pressed to abort a program.  Class DOSThread intercepts these
  240.     events and sets an internal flag.  Individual threads (or the main
  241.     program) can call the static member function "userbreak" to test
  242.     if control-break has been pressed:
  243.  
  244.         if (DOSThread::userbreak ()) ...
  245.  
  246.     The flag will remain set so that other threads can also inspect it.
  247.     Alternatively, you can use the static function "cancelbreak", which
  248.     is identical to "userbreak" except that it also resets the internal
  249.  
  250.  
  251.                                                                 Page 5
  252.  
  253.     flag.  This allows an individual thread to deal with a control-break
  254.     event without any other threads being able to deal with the same event
  255.     as well as providing a means for resetting the flag.  If threads do
  256.     not use either of these functions, control-breaks will be ignored
  257.     completely.
  258.  
  259.     Critical errors (the familiar "Abort, Retry, Fail?" errors) can be
  260.     generated by DOS if a disk is write protected or a printer is offline.
  261.     Classes derived from DOSThread can provide a virtual function "error"
  262.     to deal with any critical errors they may generate.  Threads provide
  263.     their own critical error handlers on an individual basis; the default
  264.     handler just fails the operation.  To provide a critical error handler
  265.     for a thread class, define a member function "DOSerror" as follows:
  266.  
  267.         DOSThread::Error DOSerror (int N);
  268.  
  269.     The parameter N is the DOS code defining the cause of the error.
  270.     "DOSerror" should return a result of "DOSThread::IGNORE" to ignore
  271.     the error, "DOSThread::RETRY" to retry the operation that caused the
  272.     error, or "DOSThread::FAIL" to fail the operation.  Note that during
  273.     critical-error handling, the only DOS services that you can use are
  274.     functions 00 to 0C.  Class DOSThread will intercept and ignore any
  275.     other functions, as they would otherwise cause DOS to crash.
  276.  
  277.     The function "DOSerror" should never be called directly; it will be
  278.     called automatically if an error occurs during execution of a thread.
  279.  
  280.  
  281.     6. Inspecting the status of threads.
  282.     ------------------------------------
  283.     The member function "status" allows you to determine what the
  284.     status of a thread is at any time.  It can be called as follows:
  285.  
  286.         state = thread1.status ();
  287.  
  288.     The result is a value of type DOSThread::State, which will be one
  289.     of the following values:
  290.  
  291.         DOSThread::CREATED      -- the thread is newly created and can
  292.                                    be started by calling "run".
  293.         DOSThread::READY        -- the thread is ready to run (or is
  294.                                    currently running).
  295.         DOSThread::DELAYED      -- the thread has delayed itself by
  296.                                    calling "delay".
  297.         DOSThread::WAITING      -- the thread is waiting to enter a
  298.                                    monitor function guarded by "lock".
  299.         DOSThread::QUEUED       -- the thread is inside a monitor and is
  300.                                    suspended on a monitor queue.
  301.         DOSThread::TERMINATED   -- the thread has terminated.
  302.  
  303.  
  304.     7. Using monitors for interthread communication.
  305.     ----------------------------------------------
  306.     One of the problems with multithreaded programs is communicating
  307.     between threads.  Since you do not know when a thread will be
  308.     rescheduled, it is unsafe to modify shared global variables as
  309.     it is perfectly possible for you to be interrupted during the
  310.     process of updating them.  If another thread performs a similar
  311.  
  312.  
  313.                                                                 Page 6
  314.  
  315.     update, you may well complete your update using out-of-date
  316.     values when your thread resumes, which means that the global
  317.     variables end up in an inconsistent and incorrect state.
  318.  
  319.     The base class DOSMonitor provides a basis for developing classes
  320.     which allow safe interthread communication.  All you have to do is
  321.     to derive a class from DOSMonitor which encapsulates any data which
  322.     will be updated by more than one thread and which provides access
  323.     functions to access the data.  Each access function should begin
  324.     by calling the member function "lock" and end by calling "unlock".
  325.     This will guarantee that only one thread at a time is executing an
  326.     access function in any individual monitor.  The general structure
  327.     of a monitor access function is therefore as follows:
  328.  
  329.         void MyMonitor::access ( /* parameter list */ )
  330.         {
  331.             lock ();
  332.             ...         // access shared data as required
  333.             unlock ();
  334.         }
  335.  
  336.     Classes derived from DOSMonitor can also contain instances of
  337.     class DOSMonitorQueue.  Within an access function, you can call
  338.     the member function "suspend" with a DOSMonitorQueue as its
  339.     parameter to suspend the thread executing the access function
  340.     until some condition is satisfied.  This will allow other
  341.     threads to execute access functions within that monitor.  The
  342.     other access functions can resume any threads suspended on a
  343.     particular queue by calling the member function "resume"
  344.     with the queue as a parameter.  This will reawaken the threads
  345.     suspended in that queue.
  346.  
  347.     Note that suspend should be called from within a loop; since
  348.     "resume" will restart all the threads in the specified queue,
  349.     it is not guaranteed that the condition for which the thread
  350.     is waiting will still be true at the time the thread actually
  351.     resumes execution.  Thus to suspend a thread until a counter
  352.     is non-zero, code such as the following should be used:
  353.  
  354.         while (counter != 0)
  355.             suspend (some_queue);
  356.  
  357.     As an example, consider a monitor to provide a 20-character
  358.     buffer to transfer data from one thread to another.  It might
  359.     look something like this:
  360.  
  361.         class Buffer : public DOSMonitor
  362.         {
  363.             char data[20];          // the buffer itself
  364.             int count;              // no. of chars in buffer
  365.             int in;                 // where to put next char
  366.             int out;                // where to get next char from
  367.             DOSMonitorQueue full;
  368.             DOSMonitorQueue empty;
  369.         public:
  370.             Buffer ()               { count = in = out = 0; }
  371.             void get (char& c);     // get a char from the buffer
  372.             void put (char& c);     // put a char in the buffer
  373.         };
  374.  
  375.  
  376.                                                                 Page 7
  377.  
  378.     The class constructor initialises "count" to zero to indicate an
  379.     empty buffer and sets "in" and "out" to point to the start of the
  380.     buffer.  Threads must then call "get" and "put" in order to access
  381.     the contents of the buffer.   Two DOSMonitorQueue instances are
  382.     used; "full" is used to suspend threads which call "put" when the
  383.     buffer is full, and "empty" is used to suspend threads which call
  384.     "get" when the buffer is empty.  The code for "get" would be like
  385.     this:
  386.  
  387.         void Buffer::get (char& c)
  388.         {
  389.             //--- lock the monitor against re-entry
  390.             lock ();
  391.  
  392.             //--- suspend until the buffer isn't empty
  393.             while (count == 0)
  394.                 suspend (empty);
  395.  
  396.             //--- get next character from the buffer
  397.             c = data [out++];
  398.             out %= 20;
  399.  
  400.             //--- resume any threads waiting until buffer isn't full
  401.             resume (full);
  402.  
  403.             //--- unlock the monitor to let other threads in
  404.             unlock ();
  405.         }
  406.      
  407.        
  408.     9. The class "BoundedBuffer".
  409.     -----------------------------
  410.     The class "BoundedBuffer" included in this distribution is a template
  411.     class derived from DOSMonitor which implements a bounded buffer like
  412.     the example above.  You can create a 20-character buffer using this
  413.     class as follows:
  414.  
  415.         BoundedBuffer<char> buffer(20);
  416.  
  417.     The type given in angle brackets <...> is the type of item that you
  418.     want to store in the buffer, and the parameter value is the maximum
  419.     number of items the buffer can hold.  The following member functions
  420.     are provided:
  421.  
  422.         get (item)      -- Get the next item from the buffer and store
  423.                            it in "item".  The function returns 1 (TRUE)
  424.                            if it is successful and 0 (FALSE) if the buffer
  425.                            has been closed (see below).
  426.         put (item)      -- Put a copy of "item" into the buffer.  This
  427.                            function returns 1 (TRUE) if it is successful
  428.                            and 0 (FALSE) if the buffer has been closed
  429.                            (see below).
  430.         items ()        -- Return the number of items in the buffer.
  431.         close ()        -- Close the buffer to prevent further accesses.
  432.                            If you do not close buffers when you have
  433.                            finished using them, you run the risk of your
  434.                            program never terminating -- a thread may be
  435.                            suspended waiting for a character that will
  436.                            never arrive, which means that its destructor
  437.                            will wait forever for it to terminate.
  438.  
  439.  
  440.                                                                 Page 8
  441.  
  442.     10. Error handling in monitors.
  443.     -------------------------------
  444.     Monitors derived from class DOSMonitor should provide a virtual
  445.     function called "error" which will be called if any errors are
  446.     detected in a monitor.  "Error" should be declared as follows:
  447.  
  448.         void error (DOSMonitor::ErrorCode);
  449.  
  450.     The parameter to "error" is a code for the error which has been
  451.     detected.  This can take any of the following values:
  452.  
  453.         DOSMonitor::NEW_FAIL        -- there was insufficient memory
  454.                                        to create the necessary data
  455.                                        structures for the monitor.
  456.         DOSMonitor::NO_THREAD       -- a monitor has been called when
  457.                                        there are no threads running.
  458.         DOSMonitor::LOCK_FAIL       -- the current thread is calling
  459.                                        "lock" when it has already
  460.                                        locked the monitor.
  461.         DOSMonitor::UNLOCK_FAIL     -- the current thread has called
  462.                                        "unlock" without having locked
  463.                                        the monitor.
  464.         DOSMonitor::SUSPEND_FAIL    -- the current thread has called
  465.                                        "suspend" without having locked
  466.                                        the monitor.
  467.         DOSMonitor::RESUME_FAIL     -- the current thread has called
  468.                                        "resume" without having locked
  469.                                        the monitor.
  470.  
  471.     The last five of these indicate a bug in the monitor code which
  472.     should be corrected.  The default action if a monitor does not
  473.     provide a definition for "error" is to exit the program with an
  474.     exit status in the range -1 to -6 (-1 for NEW_FAIL through to -6
  475.     for RESUME_FAIL).
  476.  
  477.  
  478.     11. Potential problem areas.
  479.     ----------------------------
  480.     Class DOSTask uses an internal monitor to guard against re-entrant
  481.     calls to DOS, as these are certain to crash your machine.  Direct
  482.     calls to BIOS functions are not protected in the same way.  While
  483.     BIOS calls are generally safer (they use the caller's stack), they
  484.     still manipulate a global shared data area.  It is therefore not
  485.     advisable to call BIOS functions directly, as this can lead to
  486.     hard-to-identify bugs resulting from an inconsistent internal
  487.     state.  However, C++ library functions normally use DOS services
  488.     rather than calling BIOS functions, so most of the functions in
  489.     the standard library are safe to use.  The major exceptions to
  490.     this are the functions defined in <bios.h> and the functions
  491.     "int86" and "int86x".
  492.  
  493.     If you do need to use BIOS functions directly, the best approach
  494.     to adopt is to localise all BIOS calls in a single monitor so that
  495.     only one task at a time can call a BIOS function; however, since
  496.     DOS services will perform their functions by making BIOS calls,
  497.     you must also use the monitor to encapsulate all DOS calls to
  498.     guarantee that only one task at a time is making a BIOS call.
  499.     This may not be a terribly practical solution.
  500.  
  501.  
  502.                                                                 Page 9
  503.  
  504.     Another point worth noting is that screen output is best done
  505.     using "fputs" rather than "cout", "printf" or "puts".  Each of
  506.     these generates several DOS calls to generate their output, and
  507.     it is therefore possible for another thread to interleave some
  508.     other output with it.  In particular, if you use "cout" it is
  509.     possible for the same output to appear twice if the thread is
  510.     interrupted after the output has been displayed but before the
  511.     internal buffer has been cleared.  The next thread which uses
  512.     "cout" will have its output appended to the existing contents
  513.     of the buffer which will then be displayed in its entirety.
  514.  
  515.     A more serious problem (which I have been completely unable to
  516.     resolve) is that programs which use direct BIOS calls can crash
  517.     the system if high memory is being used.  If your program needs
  518.     to use direct BIOS calls, you should only do this if you are
  519.     NOT using an upper memory manager such as EMM386 or QEMM.  There
  520.     is obviously some memory management context information which
  521.     needs to be saved on a thread context switch, but without any
  522.     knowledge of the internal workings of upper memory managers I
  523.     do not know how to proceed on this (and if anyone can help me
  524.     here, I will be eternally grateful!).
  525.  
  526.  
  527.     12. A plea for feedback.
  528.     ------------------------
  529.     If you use this class, please contact the author via the addresses
  530.     at the beginning; if you don't have e-mail access please send me a
  531.     postcard (I like postcards!) just to let me know you've looked at
  532.     it.  Feel free to suggest enhancements, find bugs or (better still)
  533.     fix them and send me patches.  Happy hacking!
  534.